The Developer Fastlane

« 365 days to become a developer » challenge

PHP OOP: Light and fast Content Management System

December 11, 2020

A all in-one, lighweight and fast Content Management System to share posts with your visitors. Easily create and handle posts, authors and categories. The system includes an intuitive and clean user interface.


  1. Features

    • Includes both frontent and admin interface.
    • Categories, authors and posts can be removed, edited or created by admin.
    • Authors can add a personal bio and a picture to their profile
    • Posts are sortable by categories on frontend.
    • We can create as many categories as we want, and add a short description to them (displayd will sorting posts by categories.
    • A complete "featured image" management system allows to add thumnails to posts (displayed as cards on blog main's page).
    • The featured image is shown as a background image featuring the title on post pages. If no featured image uploaded, a default clean default layout is displayd. The exerpt length will automatically adapt too on blog's main page to maintain the layout's frame
    • An image checker is included (based on max. size, image type, duplicated content).
    • The post edition includes a Markdown system to allow user to style their content. The markdown extension is made by MichelF (https://github.com/michelf/php-markdown) and added with Composer.
    • I focused on creating a smooth and nice to use UI
    • Reusable Breadcrumb system forboth frontend and admin panel
    • Object oriented code segmentation for mor modularity and to allow further improvements more easily
    • Works with a minimum of PHP files & code as short as possible (eg. Only one page is used for all post types listings: posts, categories, authors, same for common queries that are handle with one file in which we inject various parameters)
    • The database is handled with SQLite for its speed and lighweigth.
    • A more easily to handle and evolutive design, based on Bootstrap framework (no CSSS to handle, or very few)
    • Nice dashboard with a daytime adapative greeting message
    • Deletions are protected with "confirmation" popups
    • Queries errors are handled throught the Exceptions solution included in PHP
    • The Admin area is protected by a connexion system, with hashed username and password.

Go to source code

Frontend

Click on one of the images above to access the exercise's website

Backend (Admin dashboard)

Click on one of the images above to access the exercise's website

Source code

1. Frontend

blog/index.php
Code
<?php
$title = 'Blog';
require_once '../class/Post.php';
require_once '../class/PDO/BlogPDO.php';

$images_folder = '../assets/img/uploads/blog/featured_image/';
$pdo = new BlogPDO('../data/blog.db');

$error = null;
$message = null;

// Fletch categories
    try {
        $query = $pdo->query("SELECT name FROM categories ORDER BY name");
        $categories = $query->fetchAll();
    } catch (PDOException $e) {
        $error = 'PDO error: ' . $e->getMessage();
    }

// Posts sorting based on category selection
try {
    if(isset($_GET['cat']) && strtolower($_GET['cat']) !== 'all') {
        $cat = strtolower($_GET['cat']);
        $query = $pdo->query("SELECT id, title, content, date, category, featured_image FROM posts WHERE category LIKE '" . $cat . "' ORDER BY id DESC");
        $query2 = $pdo->query("SELECT description FROM categories WHERE name LIKE '" . $cat . "'");
        $posts = $query->fetchAll(PDO::FETCH_CLASS, 'Post');
        $this_category = $query2->fetch();
        if(empty($posts)) {
            $message = "No post yet in this category.";
        }
    } else {
        $query = $pdo->query("SELECT id, title, content, date, category, featured_image FROM posts ORDER BY id DESC");
        $posts = $query->fetchAll(PDO::FETCH_CLASS, 'Post');
    }
} catch (PDOException $e) {
    $error = 'PDO error: ' . $e->getMessage();
}
?>

<?php require '../includes/header.php' ?>
<?php if(isset($error)): ?>
    <div class="alert alert-danger mb-3"><?= $error ?></div>
<?php else: ?>
    <div class="d-flex justify-content-center my-5">
        <a href="index.php?cat=all">
            <button type="button" class="btn btn<?= isset($_GET['cat']) && $_GET['cat'] !== "all" ? '-outline' : '' ?>-primary btn-sm mx-1">All categories</button>
        </a>
        <?php foreach($categories as $category): ?>
            <a href="index.php?cat=<?= Post::cat_name_format($category->name) ?>">
                <button type="button" class="btn btn<?= !isset($_GET['cat']) || (isset($_GET['cat']) && $_GET['cat'] !== strtolower($category->name)) ? '-outline' : '' ?>-primary btn-sm mx-1"><?= ucfirst($category->name) ?></button>
            </a>
        <?php endforeach ?>
    </div>
    <?= !empty($this_category->description) ? '<p class="lead text-center mb-5">' . $this_category->description . '</p>' : '' ?>
    <?php if(!empty($message)): ?>
        <p class="text-center py-5 my-5"><?= $message ?></p>
    <?php else: ?>
        <div class="row">
            <?php foreach($posts as $post): ?>
                <div class="col-md-6 mb-4">
                    <div class="card">
                        <?php 
                        $has_featured_image = false;
                        if(!empty($post->featured_image)):
                            $has_featured_image = true; ?>
                            <a href="post.php?id=<?= $post->id ?>">
                                <img class="card-img-top" style="max-height: 200px; object-fit: cover;" src="<?= $images_folder . $post->featured_image ?>" alt="<?= htmlentities($post->title) ?>">
                            </a>
                        <?php endif ?>
                        <div class="card-body">
                            <a href="post.php?id=<?= $post->id ?>">
                                <h2><?= htmlentities($post->title) ?></h2>
                            </a>
                            <div class="mb-4">
                                <span class="small text-muted">
                                    <?= $post->getDate() ?>
                                </span>
                                <a href="./index.php?cat=<?= Post::cat_name_format($post->category) ?>">
                                    <span class="badge bg-light text-dark pb-1 ml-2"><?= ucfirst(htmlentities($post->category)) ?></span>
                                </a>
                            </div>
                            <?= nl2br(htmlentities($post->getExerpt($has_featured_image))) ?>
                            <div>
                                <a href="post.php?id=<?= $post->id ?>">
                                    <button class="btn btn-primary btn-sm mt-4">Read more</button>
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            <?php endforeach ?>
        </div>
    <?php endif ?>
<?php endif ?>

<?php require '../includes/footer.php'; ?>
blog/post.php
Code
<?php

$rootPath = '../';
require_once $rootPath . 'class/Post.php';
require_once $rootPath . 'class/PDO/BlogPDO.php';
require_once $rootPath . 'class/Blog/BreadcrumbBlog.php';

$pictures_folder_root = $rootPath . 'assets/img/';
$featured_images_folder = $pictures_folder_root . 'uploads/blog/featured_image/';
$author_picture_folder = $pictures_folder_root . 'uploads/blog/authors/';

$pdo = new BlogPDO($rootPath . 'data/blog.db');
$error = null;
$isset_author = false;
try {
    $query = $pdo->prepare("SELECT * FROM posts WHERE id=:id");
    $query->execute([
        'id' => $_GET['id']
    ]);
    $query->setFetchMode(PDO::FETCH_CLASS, 'Post');
    $post = $query->fetch();

    if(isset($post->author)) { // retrieve author
        $query = $pdo->query("SELECT name FROM authors WHERE name LIKE '" . $post->author . "'");
        $author = $query->fetch();
        if (!empty($author)) {
            $isset_author = true;
            $query = $pdo->query("SELECT * FROM authors WHERE name = '" . $author->name . "'");
            $author = $query->fetch();
        }
    }
} catch (PDOException $e) {
    $error = 'PDO error: ' . $e->getMessage();
}
empty($error) ? $title = htmlentities($post->title) : $title = "Post";

$bc_post = new BreadcrumbBlog ("Blog", [ ucfirst($post->category) => '?cat=' . Post::cat_name_format($post->category) ], $title);

?>

<?php require $rootPath . 'includes/header.php' ?>

<?php if(!empty($error)): ?>
    <div class="alert alert-danger"><?= $error ?></div>
<?php else: ?>
    <div class="pb-3"> <!-- post header -->
    <?php if(!empty($post->featured_image)): ?>
        <div class="card text-white border-0">
            <img class="card-img" style="max-height:18rem; object-fit: cover;" src="<?= $featured_images_folder . $post->featured_image ?>" alt="<?= $title ?>" />
            <div class="card-img-overlay d-flex flex-column justify-content-center" style="background-color: #00000060">
                <div>
                </div>
                <div>
                    <h1><?= htmlentities($post->title) ?></h1>
                    <div class="small text-capitalize"><?= isset($post->date)? $post->getDate('long') : ''  ?> - By <?= isset($post->author)? ucwords(htmlentities($post->author)) : ''  ?></div>
                    <?php if(isset($post->category)): ?>
                        <a href="./index.php?cat=<?= Post::cat_name_format($post->category) ?>">
                            <div class="badge bg-light text-dark pb-1 mt-3">
                                <?= Post::cat_name_format($post->category) ?>
                            </div>
                        </a>
                    <?php endif ?>
                </div>
            </div>
        </div>
    <?php else: ?>
        <h1><?= $title ?></h1>
        <div class="small"><?= isset($post->date)? $post->getDate('long') : ''  ?> - By <?= isset($post->author)? $post->author : ''  ?></div>
        <hr>
    <?php endif; ?>

    <!-- breadcrumb -->
    <div class="small text-muted my-2"><?= $bc_post->show_breadcrumb() ?></div>
<hr>

    <div class="mt-4"> <!-- post content -->
        <p class="lead mt-5"><?= isset($post->introduction)? htmlentities($post->introduction) : ''  ?></p>
        <div class="my-5">
            <?= isset($post->content)? nl2br(htmlentities($post->content)) : ''  ?>
        </div>
    </div>
    <div> <!-- post footer-->
        <?php if ($isset_author): ?>
            <hr>
            <div class="card bg-light mt-5" style="max-width: 30rem;"> <!-- author box -->
                <div class="card-header small text-muted text-capitalize">About <?= ucwords(htmlentities($author->name)) ?></div>
                <div class="media card-body align-items-center">
                    <img class="mr-3" style="border-radius:100%; width: 4rem; height:auto" src="<?= !empty($author->picture)? $author_picture_folder . $author->picture : $pictures_folder_root . 'avatar-default.png'  ?>" />
                    <div class="media-body">
                        <?= !empty($author->bio)? htmlentities($author->bio) : 'I am a contributor to the blog. From time to time, i share my expertise with you through an article.' ?>
                    </div>
                </div>
            </div>
        <?php endif ?>
    </div>  
</div>




<?php endif ?>

<?php require $rootPath . 'includes/footer.php' ?>

2. Backend

admin/index.php
Code
<?php
require_once '../includes/functions/login.php';
redirect_if_not_connected('../pages/login.php');

require_once '../includes/functions/admin_dashboard.php';

$title = greetings(credentials()['username']);
require '../includes/header.php';

?>

<p class="lead mb-4">What do you want to do?</p>
<div class="row pt-3 pb-3">
    <?php foreach(admin_dashboard_tiles() as $tile): ?>
        <div class="col-sm-3" style="align-items: stretch">
            <a href="<?= $tile['link'] ?>" class="link-unstyled">
                <div class="<?= 'card ' . $tile['color'] . ' mb-3'?>">
                    <div class="card-header small"><?= $tile['header'] ?></div>
                    <div class="card-body" style="display:flex; flex-direction:column; justify-content:space-between">
                        <div>
                            <h5 class="card-title"><?= $tile['title'] ?></h5>
                            <p class="card-text small"><?= $tile['text'] ?></p>
                        </div>
                        <div class="<?= 'btn ' . $tile['btn-color'] . ' btn-block mt-3'?>"><?= $tile['button'] ?></div>
                    </div>
                </div>
            </a>
        </div>
    <?php endforeach ?>
</div>

<?php require '../includes/footer.php'; ?>

Post-types listings

admin/blog/index.php
Code
<?php
$rootPath = '../../';
$modals_id = [];
require_once $rootPath . "includes/functions/login.php";
require_once $rootPath . "class/Blog/PostTypeTable.php";
require_once $rootPath . "class/Blog/BreadcrumbBlog.php";

redirect_if_not_connected($rootPath . 'pages/login.php');

$db_tables = [
    'posts' => [
        'fields' => ['id', 'title'],
        'order_by' => 'id DESC',
        'singular' => 'post',
        'main_field' => 'title',
    ],
];

$index = new PostTypeTable($db_tables, $rootPath);
$index->getPostTypeTable();

require $rootPath . 'includes/footer.php';
admin/blog/index-cat-auth.php
Code
<?php
$rootPath = '../../';
$modals_id = $sections = $tables = [];
require_once $rootPath . "includes/functions/login.php";
require_once $rootPath . "class/Blog/PostTypeTable.php";
require_once $rootPath . "class/Blog/BreadcrumbBlog.php";

redirect_if_not_connected($rootPath . 'pages/login.php');

$db_tables = [
    'categories' => [
        'fields' => ['id', 'name'],
        'order_by' => 'id ASC',
        'singular' => 'category',
        'main_field' => 'name',
    ],
    'authors' => [
        'fields' => ['id', 'name'],
        'order_by' => 'id',
        'singular' => 'author',
        'main_field' => 'name',
    ]
];

$index = new PostTypeTable($db_tables, $rootPath);
$index->getPostTypeTable(false);

require '../../includes/footer.php';

Editing pages

admin/blog/edit-post.php
Code
<?php

$rootPath = '../../';
require_once $rootPath . "includes/functions/login.php";
require_once $rootPath . 'class/PDO/BlogPDO.php';
require_once $rootPath . 'class/Blog/BreadcrumbBlog.php';

redirect_if_not_connected($rootPath . 'pages/login.php');

// Start editing

$pt_singular = 'post';
$pt_plural = 'posts';
$main_field = 'title';
$prepare_vars = 'title, introduction, content, date, author, category';
$has_upload = true;
$picture_field_name = 'featured_image';
if(isset($_POST['title'], $_POST['content'])) {
    $execute_command = [
        // "picture" and "id" lines are automatically added by the script (so don't add them here)
        'title' => $_POST['title'],
        'introduction' => $_POST['introduction'],
        'content' => $_POST['content'],
        'date' => time(),
        'author' => $_POST['author'],
        'category' => $_POST['category'],
    ];
}
$index_redirection = 'index.php';

// Stop editing

require './parts/edit-queries.php'; // SQL Queries
require './parts/' . strtolower($pt_singular) . '-form.php'; // Form (Edit & Add new)

require $rootPath . 'includes/footer.php';
admin/blog/edit-category.php
Code
<?php

$rootPath = '../../';
require_once $rootPath . "includes/functions/login.php";
require_once $rootPath . 'class/PDO/BlogPDO.php';
require_once $rootPath . 'class/Blog/BreadcrumbBlog.php';

redirect_if_not_connected($rootPath . 'pages/login.php');

// Start editing

$pt_singular = 'category';
$pt_plural = 'categories';
$main_field = 'name';
$prepare_vars = 'name, description';
$has_upload = false;
$picture_field_name = '';
if(isset($_POST['name'])) {
    $execute_command = [
        // "picture" and "id" lines are automatically added by the script (so don't add them here)
        'name' => $_POST['name'],
        'description' => $_POST['description']
    ];
}
$index_redirection = 'index-cat-auth.php';

// Stop editing

require './parts/edit-queries.php'; // SQL Queries
require './parts/' . strtolower($pt_singular) . '-form.php'; // Form (Edit & Add new)

require $rootPath . 'includes/footer.php';
admin/blog/edit-author.php
Code
<?php

$rootPath = '../../';
require_once $rootPath . "includes/functions/login.php";
require_once $rootPath . 'class/PDO/BlogPDO.php';
require_once $rootPath . 'class/Blog/BreadcrumbBlog.php';

redirect_if_not_connected($rootPath . 'pages/login.php');

// Start editing

$pt_singular = 'author';
$pt_plural = 'authors';
$main_field = 'name';
$prepare_vars = 'name, bio';
$has_upload = true;
$picture_field_name = 'picture';
if(isset($_POST['name'], $_POST['bio'])) {
    $execute_command = [
        // "picture" and "id" lines are automatically added by the script (so don't add them here)
        'name' => $_POST['name'],
        'bio' => $_POST['bio']
    ];
}
$index_redirection = 'index-cat-auth.php';

// Stop editing

require './parts/edit-queries.php'; // SQL Queries
require './parts/' . strtolower($pt_singular) . '-form.php'; // Form (Edit & Add new)

require $rootPath . 'includes/footer.php';

Parts (forms)

admin/blog/parts/post-form.php
Code
<?php

    $pictures_folder = $rootPath . 'assets/img/uploads/blog/featured_image/';
    require_once $rootPath . 'class/FileUpload/ImageUploader.php';
    require_once $rootPath . 'class/Modals/Modal.php';

// Fill select lists with authors and categories

    $pdo = new BlogPDO('../../data/blog.db');
    $error = null;
    try {
        $query = $pdo->query("SELECT name FROM categories ORDER BY id ASC");
        $categories = $query->fetchAll();
        $query = $pdo->query("SELECT name FROM authors ORDER BY id ASC");
        $authors = $query->fetchAll();
    } catch (PDOException $e) {
        $error = $e->getMessage();
    }
    if(isset($_FILES["fileToUpload"])) {
        $upload_image = new ImageUploader($_FILES["fileToUpload"], $pictures_folder);
        $upload_image->upload_no_check();
    }
?>
<form action="" method="post" enctype="multipart/form-data">
    <div class="row">
        <div class="col col-md-8">
            <p class="lead mb-5">Fields with an asterisk are mandatory.</p>
            <div class="form-group">
                <label>
                    Post title*
                    <small class="small text-muted form-text">60 characters max. are recommended</small>
                </label>
                <input type="text" name="title" class="form-control" required value="<?= $isEdit && isset($postType)? $postType->title : '' ?>">
                </div>
            <div class="form-group">
                <label>
                    Introduction
                    <div class="small text-muted">This text will be displayed in bigger letters than the content</div>
                </label>
                <textarea rows="3" class="form-control" name="introduction"><?= $isEdit && isset($postType->introduction) ? $postType->introduction : '' ?></textarea>
            </div>
            <div class="form-group">
                <label>
                    Content*
                    <div class="small text-muted">HTML not allowed for security purpose</div>
                </label>
                <textarea rows="15" class="form-control" name="content" required><?= $isEdit && isset($postType)? $postType->content : '' ?></textarea>
            </div>            
        </div>
        <div class="col col-md-4">
            <div class="sticky-top pt-4">
                <button type="submit" class="btn btn-primary btn-block mb-3">
                    <svg width="1.2em" height="1.2em" viewBox="0 0 16 16" class="bi bi-check2" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>
                    Publish
                </button>
                <div class="d-flex mb-5">
                    <?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
                        <a class="flex-grow-1 mr-3" href="<?= BreadcrumbBlog::PROJECT_ROOT . 'blog/' . $pt_singular . '.php?id=' . $postType->id ?>">
                            <button type="button" class="btn btn-outline-primary w-100">
                                <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-eye" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.134 13.134 0 0 0 1.66 2.043C4.12 11.332 5.88 12.5 8 12.5c2.12 0 3.879-1.168 5.168-2.457A13.134 13.134 0 0 0 14.828 8a13.133 13.133 0 0 0-1.66-2.043C11.879 4.668 10.119 3.5 8 3.5c-2.12 0-3.879 1.168-5.168 2.457A13.133 13.133 0 0 0 1.172 8z"/><path fill-rule="evenodd" d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/></svg>
                                View
                            </button>
                        </a>
                        <?php $modal_delete = new Modal ($pt_singular . '-del-confirm'); ?>
                        <?php $modal_delete->showModalTrigger($postType->id, 
                        '<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
                        Delete', 'button', 'btn btn-outline-danger flex-grow-1') ?>
                    <?php endif ?>
                </div>
                <div class="form-group">
                    <label>Category</label> 
                    <select name="category" class="form-control">
                        <?php foreach($categories as $category): ?>
                            <option value="<?= $category->name ?>" <?= $isEdit && isset($postType->category) && $category->name ===  $postType->category ? 'selected' : '' ?> >
                                <?= ucwords($category->name) ?>
                            </option>
                        <?php endforeach ?>
                    </select>
                </div>
                <div class="form-group">
                    <label>Featured image</label>
                    <input type="file" name="fileToUpload" id="fileToUpload" accept="image/*"> 
                    <?php if ($isEdit && isset($postType) && !empty($postType->featured_image)): ?>
                        <div class="mt-2" style="position: relative">
                            <div style="color: white; padding: 5px 10px; background-color: #00000080; position: absolute; right: 0">
                                <label style="margin:0">
                                    <input type="checkbox" name="file-delete" class="mr-1">
                                    Remove file
                                </label>
                            </div>
                        </div>
                        <img src="<?= $pictures_folder . $postType->featured_image ?>" style="width:100%" />
                    <?php endif ?>
                </div> 
                <div class="form-group">
                    <label>Author</label>
                    <select name="author" class="form-control">
                        <?php foreach($authors as $author): ?>
                            <option value="<?= $author->name ?>" <?= $isEdit && isset($postType->author) && $postType->author ===  $author->name ? 'selected' : '' ?> >
                                <?= ucwords($author->name) ?>
                            </option>
                        <?php endforeach ?>
                    </select>
                </div>
            </div>
        </div>
    </div>
</form>
<?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
    <?php $modal_delete->showModal($postType->id, './delete.php?type=' . $pt_singular . '&id=' . $postType->id); ?>
<?php endif;
admin/blog/parts/category-form.php
Code
<?php
$pictures_folder = $rootPath . 'assets/img/uploads/blog/featured_image/';
require_once $rootPath . 'class/Modals/Modal.php';

?>
<form action="" method="post">
    <div class="row">
        <div class="col col-md-8">
            <p class="lead mb-5">Fields with an asterisk are mandatory.</p>
            <div class="form-group">
                <label>
                    Category name*
                    <small class="small text-muted form-text">1 or 2 words max. are recommended</small>
                </label>
                <input type="text" name="name" class="form-control" required value="<?= isset($postType)? $postType->name : '' ?>">
                </div>
            <div class="form-group">
                <label>
                    Description
                    <div class="small text-muted">A short text that describes the category's content.</div>
                </label>
                <textarea rows="3" class="form-control" name="description"><?= isset($postType->description) ? $postType->description : '' ?></textarea>
            </div>
        </div>
        <div class="col col-md-4">
            <div class="sticky-top pt-4">
                <div class="d-flex mb-5">
                    <button type="submit" class="btn btn-primary flex-grow-1">
                        <svg width="1.2em" height="1.2em" viewBox="0 0 16 16" class="bi bi-check2" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>
                        Publish
                    </button>
                    <?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
                        <?php $modal_delete = new Modal ($pt_singular . '-del-confirm'); ?>
                        <?php $modal_delete->showModalTrigger($postType->id, 
                        '<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>',
                        'button', 'btn btn-outline-danger ml-3') ?>
                    <?php endif ?>
                </div>
            </div>
        </div>
    </div>
</form>
<?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
    <?php $modal_delete->showModal($postType->id, './delete.php?type=' . $pt_singular . '&id=' . $postType->id); ?>
<?php endif; ?>
admin/blog/parts/author-form.php
Code
<?php

$pictures_folder = $rootPath . 'assets/img/uploads/blog/authors/';
require_once $rootPath . 'class/FileUpload/ImageUploader.php';
require_once $rootPath . 'class/Modals/Modal.php';

if(isset($_FILES["fileToUpload"])) {
    $upload_image = new ImageUploader($_FILES["fileToUpload"], $pictures_folder);
    $upload_image->upload_no_check();
}
?>
<form action="" method="post" enctype="multipart/form-data">
    <div class="row">
        <div class="col col-md-8">
            <p class="lead mb-5">Fields with an asterisk are mandatory.</p>
            <div class="form-group">
                <label>
                    Name*
                    <small class="small text-muted form-text">30 characters max. recommended</small>
                </label>
                <input type="text" name="name" class="form-control" required value="<?= isset($postType)? $postType->name : '' ?>">
                </div>
            <div class="form-group">
                <label>
                    Bio
                    <div class="small text-muted">Describe yourself in a few words.</div>
                </label>
                <textarea rows="3" class="form-control" name="bio"><?= isset($postType->bio) ? $postType->bio : '' ?></textarea>
            </div>          
        </div>
        <div class="col col-md-4">
            <div class="sticky-top pt-4">
            <div class="d-flex mb-5">
                    <button type="submit" class="btn btn-primary flex-grow-1">
                        <svg width="1.2em" height="1.2em" viewBox="0 0 16 16" class="bi bi-check2" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>
                        Publish
                    </button>
                    <?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
                        <?php $modal_delete = new Modal ($pt_singular . '-del-confirm'); ?>
                        <?php $modal_delete->showModalTrigger($postType->id, 
                        '<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>',
                        'button', 'btn btn-outline-danger ml-3') ?>
                    <?php endif ?>
                </div>
                <div class="form-group">
                    <label>Picture / Avatar</label><br>
                    <input type="file" name="fileToUpload" id="fileToUpload" accept="image/*"> 
                    <?php if (isset($postType) && !empty($postType->picture)): ?>
                        <div class="mt-2" style="position: relative">
                            <div style="color: white; padding: 5px 10px; background-color: #00000080; position: absolute; right: 0">
                                <label style="margin:0">
                                    <input type="checkbox" name="file-delete" class="mr-1">
                                    Remove file
                                </label>
                            </div>
                        </div>
                        <img src="<?= $pictures_folder . $postType->picture ?>" style="width:100%" />
                    <?php endif ?>
                </div> 
            </div>
        </div>
    </div>
</form>
<?php if(empty($_GET['p']) || $_GET['p'] !== 'add'): ?>
    <?php $modal_delete->showModal($postType->id, './delete.php?type=' . $pt_singular . '&id=' . $postType->id); ?>
<?php endif; ?>

Parts (queries)

admin/blog/parts/edit-queries.php
Code
<?php

$isEdit = true;
if(!empty($_GET['p']) && $_GET['p'] === "add") {
    $isEdit = false;
}
$pdo = new BlogPDO($rootPath . 'data/blog.db');
$error = null;
$success = null;

try {
    // ADD NEW
    if(!$isEdit) {
        if (!empty($_POST[$main_field])) {
            if ($has_upload) {
                $prepare_vars .= ', ' . $picture_field_name;
            }
            $prepare_vars_ids = ":" . str_replace(',', ', :', str_replace(' ', '', $prepare_vars));
            isset($_FILES['fileToUpload']['name']) ? $picture_name = $_FILES['fileToUpload']['name'] : $picture_name = null;
            $query = $pdo->prepare("INSERT INTO " . $pt_plural . " (" . $prepare_vars . ") VALUES (" . $prepare_vars_ids . ")");
            if ($has_upload) { // Add 1 variable
                $execute_command[$picture_field_name] = $picture_name; 
            }
            $query->execute($execute_command);
            header("location:./edit-" . strtolower($pt_singular) . ".php?id=" . $pdo->lastInsertId() . "&i=new");
        }
    // EDIT
    } else {
        // Fill "value" fields attribute for this post
        $query = $pdo->prepare("SELECT * FROM " . $pt_plural . " WHERE id = :id");
        $query->execute([ 'id' => $_GET['id'] ]);
        $postType = $query->fetch(); // $postType is used to prefill the form inside edit-*.php
        // Update data
        if (!empty($_POST[$main_field])) {
            // File upload conditions
                if ($has_upload) {
                    $picture_name = $_FILES['fileToUpload']['name'];
                    // To not replace $picture_name by '' while updating post without choising a new image:
                    if ($picture_name === '' && !isset($_POST['file-delete'])) { 
                        $query = $pdo->prepare("SELECT " . $picture_field_name . " FROM " . $pt_plural . " WHERE id = :id");
                        $query->execute([ 'id' => $_GET['id'] ]);
                        $picture = $query->fetch();
                        $picture_name = $picture->$picture_field_name;
                    } elseif (isset($_POST['file-delete']) && $_POST['file-delete'] === 'on') { // If "Remove file" is checked
                        $picture_name = null;
                    }
                }
            // Update query
                // Generate vars list ($ptResult) from $prepare_vars
                    if ($has_upload) { // Add 1 variable
                        $prepare_vars .= ", " . $picture_field_name;
                    }
                    $ptInput = (array)explode(',', str_replace(' ', '', $prepare_vars));
                    foreach($ptInput as $ptItem) { 
                        $ptItem = $ptItem . ' = :' . $ptItem; $ptResult[] = $ptItem; 
                    }
                    $ptResult = implode(', ', $ptResult);
                $query = $pdo->prepare("UPDATE " . $pt_plural . " SET " . $ptResult . " WHERE id = :id");
                if ($has_upload) { // Add 2 variables
                    $execute_command[$picture_field_name] = $picture_name;
                }
                $execute_command['id'] = $_GET['id'];
                $query->execute($execute_command);
            // Redirect
            header("location:./edit-" . strtolower($pt_singular) . ".php?id=" . $_GET['id'] . "&i=edit");
        }
    }
} catch (PDOException $e) {
    $error = ucfirst($pt_singular) . ' was not saved because of an error:<br><small>' . $e->getMessage() . '</small>';
}
if (!empty($_GET['i'])) {
    $success = ucfirst($pt_singular) . ' published successfully. Go back to <a href="' . $rootPath . 'admin/blog/' . $index_redirection . '">list</a>';
}

$isEdit ? $title = "Edit " . strtolower($pt_singular) : $title = "Add a new " . strtolower($pt_singular);

// Breadcrumb
$bc_post = new BreadcrumbBlog ("Admin", [ ucfirst($pt_plural) => 'blog/' . $index_redirection ], $title);

require $rootPath . 'includes/header.php';
?>
<!-- breadcrumb -->
<div class="small text-muted my-2"><?= $bc_post->show_breadcrumb() ?></div>
<hr>

<?php
if(!empty($error)): ?>
    <div class="alert alert-danger"><?= $error ?></div>
<?php elseif(isset($success)): ?>
    <div class="alert alert-success"><?= $success ?></div>
<?php endif;

3. Classes

class/BlogPDO.php
Code
<?php

class BlogPDO extends PDO {

    public function __construct($dns, $username = null, $password = null, $type = 'sqlite') {
        parent::__construct($type . ':' . $dns, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ
        ]);
    }
    public function pluralize(string $word)
    {
        switch($word) {
            case 'post': return 'posts'; break;
            case 'category': return 'categories'; break;
            case 'author': return 'authors'; break;
            default: return '';
        }
    }
}
class/PostTypeTable.php
Code
<?php
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR;
require_once $root . 'PDO' . DIRECTORY_SEPARATOR . 'BlogPDO.php';
require_once $root . 'Modals' . DIRECTORY_SEPARATOR . 'Modal.php';

class PostTypeTable {

    public function __construct(array $db_tables, string $rootPath)
    {
        $this->db_tables = $db_tables;
        $this->rootPath = $rootPath;
    }

    public function getPostTypeTable(bool $show_view_btn = true) 
    {
        $error = null;
        $pdo = new BlogPDO($this->rootPath . 'data/blog.db');

        foreach($this->db_tables as $table_name => $data) {
            $fields = implode(', ', $data['fields']);
            try {
                $query = $pdo->query("SELECT " . $fields . " FROM " . $table_name . " ORDER BY " . $data['order_by']);
                $tables[$table_name] = $query->fetchAll();
                if(isset($_GET['id']) && isset($_GET['info'])) {
                    if($_GET['info'] === 'delete-cat') {
                        $query = $pdo->prepare("DELETE FROM " . $table_name . " WHERE id = :id");
                        var_dump($query);
                        $query->execute([
                            'id' => $_GET['id']
                        ]);
                    }
                }
            } catch (PDOException $e) {
                $errors[] = $data['singular'] . ' not deleted because of a query error:<br><small>' . $e->getMessage() . '<br><pre>' . var_dump($query) . '</pre></small>';
            }
            if(isset($_GET['info'])) {
                if($_GET['info'] === 'del-success') {
                    $success = ucfirst($_GET['type']) . " deleted successfully";
                } elseif($_GET['info'] === 'del-noid') {
                    $error = ucfirst($_GET['type']) . " was not deleted because no post ID were submited. Please delete posts from <a href='./index.php'>here</a>";
                } elseif($_GET['info'] === 'del-error') {
                    $error = ucfirst($_GET['type']) . " not deleted because of a SQL error: " . $_GET['msg'];
                }
            }
            $sections[] = $table_name;
        }
        if(count($sections) > 1) {
            $title_str = ucwords(implode(', ', $sections));
            $title = substr_replace($title_str, ' & ', strrpos($title_str, ', '), 2);
        } else {
            $title = ucwords($sections[0]);
        }

        // Breadcrumb
        $bc_post = new BreadcrumbBlog ("Admin", [], $title);

        require $this->rootPath . 'includes/header.php'; ?>

        <!-- breadcrumb -->
        <div class="small text-muted my-2"><?= $bc_post->show_breadcrumb() ?></div>
<hr>

        <? if(!empty($error)) {
            echo '<div class="alert alert-danger mb-3">' . $error . '</div>';
        } elseif(!empty($success)) {
            echo '<div class="alert alert-success mb-3">' . $success . '</div>';
        }

        $col_width = 12;
        if(count($this->db_tables) > 1) {
            $col_width = 6;
        }
        ?>
        <p class="lead mb-5">Choose an item to edit.</p>

        <div class="row mt-5">
            <?php foreach ($tables as $table_name => $table_items):
                $table_name_singular = $this->db_tables[$table_name]['singular'];
                $main_field = (string)$data['main_field'] ?>
                <div class="col-md-<?= $col_width ?> mb-4">
                    <div class="d-flex align-items-center mb-3">
                        <h2><?= ucfirst($table_name) ?></h2>
                        <a href="<?= $this->rootPath . 'admin/blog/edit-' . $table_name_singular . '.php?p=add' ?>">
                            <button class="btn btn-primary ml-3">Add new</button>
                        </a>
                    </div>
                    <div class="card">
                        <table class="table">
                        <thead class="thead-light">
                            <tr>
                                <th>Post</th>
                                <th>Options</th>
                                <th>ID</th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach($table_items as $item): 
                                $modal_delete[$table_name] = new Modal ($table_name . '-del-confirm');
                                $modals_id[$table_name][] = $item->id ?>
                                <tr>
                                    <td style="vertical-align: middle"><?= htmlentities($item->$main_field) ?></td>
                                    <td style="vertical-align: middle">
                                        <div class="my-1">
                                            <a href="<?= $this->rootPath . 'admin/blog/edit-' . $table_name_singular . '.php?id=' . $item->id ?>">
                                                <button class="btn btn-primary btn-sm mr-2">Edit</button>
                                            </a>
                                            <?php if($show_view_btn): ?>
                                                <a href="<?= $this->rootPath . 'blog/' . $table_name_singular . '.php?id=' . $item->id ?>">
                                                    <button class="btn btn-success btn-sm mr-2">View</button>
                                                </a>
                                            <?php endif ?>
                                            <?php $modal_delete[$table_name]->showModalTrigger($item->id, 'Delete', 'button', 'btn-danger btn-sm') ?>
                                        </div>
                                    </td>
                                    <td class="small text-muted" style="vertical-align: middle">#<?= $item->id ?></td>
                                </tr>
                            <?php endforeach ?>
                        </tbody>
                        </table>
                    </div>
                </div>
            <?php endforeach ?>
        </div>
        <?php
        foreach($this->db_tables as $table_name => $data) {
            foreach($modals_id[$table_name] as $modal_id) {
                $modal_delete[$table_name]->showModal($modal_id, './delete.php?type=' . $data['singular'] . '&id=' . $modal_id);
            }
        }
    }
}
class/Post.php
Code
<?php

class Post {

    public function getDate(string $length = '')
    {
        $date = new DateTime("@" . $this->date);
        switch($length) {
            case 'long': $f = "F j, Y"; break;
            case 'short': $f = "m/d/Y"; break;
            default: $f = "M j, Y";
        }
        return $date->format("M j, Y");
    }
    public function getExerpt(bool $has_featured_image = true)
    {
        $has_featured_image ? $max_letters = 200 : $max_letters = 500;
        if(strlen($this->content) > $max_letters) {
            $exerpt = wordwrap($this->content, $max_letters, '__break__');
            $array = explode('__break__', $exerpt);
            return $array[0] . ' ...';
        } else {
            return $this->content;
        }
    }
    public static function cat_name_format(string $category_name) 
    {
        return strtolower(str_replace(['-','_',' '], '-', $category_name));
    }

}
class/BreadcrumbBlog.php
Code
<?php

class BreadcrumbBlog {

    public const PROJECT_ROOT = DIRECTORY_SEPARATOR . 'projects' . DIRECTORY_SEPARATOR . 'php-playground' . DIRECTORY_SEPARATOR;

    private const LAST_PART_LENGTH = 30; // Letters
    private const DELIMITER = '>';

    public function __construct(string $root_step_title, array $middle_steps, string $current_page_title) {
        $this->root_step_title = $root_step_title;
        $this->middle_steps = $middle_steps;
        $this->current_page_title = $current_page_title;
    }
    public function show_breadcrumb()
    {
        $root_folder = self::PROJECT_ROOT . strtolower($this->root_step_title);
        $root_step = '<a href="' . $root_folder . '">' . $this->root_step_title . '</a> ' . self::DELIMITER . ' ';
        $middle_steps = '';
        foreach ($this->middle_steps as $bc_title => $bc_link) {
            $middle_steps .= '<a href="' . $root_folder . DIRECTORY_SEPARATOR . $bc_link . '">' . $bc_title . '</a> ' . self::DELIMITER . ' ';
        }
        $last_step = $this->get_bc_exerpt();
        return $root_step . $middle_steps . $last_step ;
    }
    
    private function get_bc_exerpt()
    {
        if(strlen($this->current_page_title) > 30) {
            $exerpt = wordwrap($this->current_page_title, self::LAST_PART_LENGTH, '__break__');
            $array = explode('__break__', $exerpt);
            return ucfirst($array[0]) . ' ...';
        } else {
            return $this->current_page_title;
        }
    }

}
class/Modal.php
Code
<?php 

class Modal { 

    function __construct(string $id)
    {
        $this->modal_group_name = $id;
    }
    public function showModalTrigger(string $post_type_id, string $title, string $link_type = "button", string $class = "btn-primary"): void
    {
        if ($link_type === "button") { ?>
            <button type="button" class="btn <?= $class ?>" data-toggle="modal" data-target="#<?= $this->modal_group_name . '-' . $post_type_id ?>">
                <?= $title ?>
            </button><?php
        } else { ?>
            <a href="#" class="<?= $class ?>">
                <?= $title ?>
            </a><?php 
        }
    }
    public function showModal(string $post_type_id, string $confirm_link, string $title = 'Are you sure?', string $content = 'The content will be deleted permanently, with no way to recover.'): void
    {
        ?>
        <div class="modal fade" id="<?= $this->modal_group_name . '-' . $post_type_id ?>" tabindex="-1" role="dialog" >
            <div class="modal-dialog modal-dialog-centered" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title"><?= $title ?></h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <?= $content ?>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <a href="<?= $confirm_link ?>"><button type="button" class="btn btn-primary">Confirm</button></a>
                    </div>
                </div>
            </div>
        </div>
        <?php
    }
}
© 2020 - Edouard Proust | The Developer Fastlane